package com.sz.common.base.dao;

import com.sz.common.base.cache.redis.RedisPoolManager;
import com.sz.common.base.utils.SerializeUtil;
import org.apache.ibatis.builder.SqlSourceBuilder;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.CachingExecutor;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.ReflectorFactory;
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
import org.apache.ibatis.reflection.factory.ObjectFactory;
import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.springframework.beans.BeanUtils;
import redis.clients.jedis.Jedis;

import java.sql.SQLException;
import java.util.List;
import java.util.Properties;


/**
 * Function: 采用分页拦截器执行物理分页查询时，原生的Executor创建cacheKey时未能包含分页参数page，为了解决这个问题，创建了本拦截器，
 * 本拦截器会拦截CachingExecutor的query方法，在创建cacheKey时将分页参数page包含其中。 老规矩，签名里要拦截的类型只能是接口。
 * * Author: hpeng.wang <br>
 * Date: 2017-03-17 21:00:00
 */

@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class CacheInterceptor implements Interceptor {

    private static final Log logger = LogFactory.getLog(CacheInterceptor.class);
    private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
    private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
    private static final ReflectorFactory DEFAULT_REFLECTOR_FACTORY = new DefaultReflectorFactory();
    public static final String H = "h";
    public static final String TARGET = "target";
    public static final String PAGE = "page";
    public static final String COMM = ".";
    public static final String PAGINATION = ":pagination";
    public static final String SELECT = "select";

    private static String defaultPageSqlId = ".*ListPage.*"; // 需要拦截的ID(正则匹配)
    private static String pageSqlId = ".*ListPage.*"; // 需要拦截的ID(正则匹配)

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Executor executorProxy = (Executor) invocation.getTarget();
        MetaObject metaExecutor = MetaObject.forObject(executorProxy, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_REFLECTOR_FACTORY);
        // 分离代理对象链
        while (metaExecutor.hasGetter(H)) {
            Object object = metaExecutor.getValue(H);
            metaExecutor = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_REFLECTOR_FACTORY);
        }
        // 分离最后一个代理对象的目标类
        while (metaExecutor.hasGetter(TARGET)) {
            Object object = metaExecutor.getValue(TARGET);
            metaExecutor = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_REFLECTOR_FACTORY);
        }
        Object[] args = invocation.getArgs();
        return this.query(metaExecutor, args);
    }

    public <E> List<E> query(MetaObject metaExecutor, Object[] args) throws SQLException {
        MappedStatement ms = (MappedStatement) args[0];
        Object parameterObject = args[1];
        RowBounds rowBounds = (RowBounds) args[2];
        ResultHandler resultHandler = (ResultHandler) args[3];
        BoundSql boundSql = ms.getBoundSql(parameterObject);
        CacheKey cacheKey = createCacheKey(ms, parameterObject, rowBounds, boundSql);
        Executor executor = (Executor) metaExecutor.getOriginalObject();
        List<E> list = executor.query(ms, parameterObject, rowBounds, resultHandler, cacheKey, boundSql);

        MetaObject metaObject = MetaObject.forObject(parameterObject, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_REFLECTOR_FACTORY);
        //有分页
        if (ms.getId().matches(pageSqlId)
                && metaObject.hasGetter(PAGE)
                && RedisPoolManager.isRedisUp()
                && ms.getCache() != null) {
            //获取分页
            //来自缓存-没有分页相关的信息，设置到缓存中
            Jedis jedis = RedisPoolManager.getJedis();
            String nameSpace = ms.getId().substring(0, ms.getId().lastIndexOf(COMM));
            byte[] bytes = jedis.hget(nameSpace.getBytes(), (cacheKey.toString() + PAGINATION).getBytes());
            //设置进去
            if (bytes == null) {
                Pagination page = (Pagination) metaObject.getValue(PAGE);
                jedis.hset(nameSpace.getBytes(), (cacheKey.toString() + PAGINATION).getBytes(), SerializeUtil.serialize(page));
            } else {
                Pagination pagination = (Pagination) metaObject.getValue(PAGE);
                Pagination redisPagination = (Pagination) SerializeUtil.unserialize(bytes);
                BeanUtils.copyProperties(redisPagination, pagination);
            }
            jedis.close();
        }
        return list;
    }

    private CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
        Configuration configuration = ms.getConfiguration();
        CacheKey cacheKey = new CacheKey();
        cacheKey.update(ms.getId());
        cacheKey.update(rowBounds.getOffset());
        cacheKey.update(rowBounds.getLimit());
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        // 解决自动生成SQL，SQL语句为空导致key生成错误的bug
        if (null == boundSql.getSql() || "".equals(boundSql.getSql())) {
            String id = ms.getId();
            id = id.substring(id.lastIndexOf(COMM) + 1);
            String newSql = null;
            try {
                SqlSource sqlSource = buildSqlSource(configuration, newSql, parameterObject.getClass());
                parameterMappings = sqlSource.getBoundSql(parameterObject).getParameterMappings();
                cacheKey.update(newSql);
            } catch (Exception e) {
                logger.error("Update cacheKey error.", e);
            }
        } else {
            cacheKey.update(boundSql.getSql());
        }

        MetaObject metaObject = MetaObject.forObject(parameterObject, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_REFLECTOR_FACTORY);

        if (parameterMappings.size() > 0 && parameterObject != null) {
            TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                cacheKey.update(parameterObject);
            } else {
                for (ParameterMapping parameterMapping : parameterMappings) {
                    String propertyName = parameterMapping.getProperty();
                    if (metaObject.hasGetter(propertyName)) {
                        cacheKey.update(metaObject.getValue(propertyName));
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        cacheKey.update(boundSql.getAdditionalParameter(propertyName));
                    }
                }
            }
        }
        // 当需要分页查询时，将page参数里的当前页和每页数加到cachekey里
        if (ms.getId().matches(pageSqlId) && metaObject.hasGetter(PAGE)) {
            Pagination page = (Pagination) metaObject.getValue(PAGE);
            if (null != page) {
                cacheKey.update(page.getCurrentPage());
                cacheKey.update(page.getPageSize());
            }
        }
        return cacheKey;
    }

    /**
     * 只拦截CachingExecutor，其他的直接返回目标本身
     */
    @Override
    public Object plugin(Object target) {
        if (target instanceof CachingExecutor) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    @Override
    public void setProperties(Properties properties) {

    }

    private SqlSource buildSqlSource(Configuration configuration, String originalSql, Class<?> parameterType) {
        SqlSourceBuilder builder = new SqlSourceBuilder(configuration);
        return builder.parse(originalSql, parameterType, null);
    }

}